Passed
Branch wavefile-rw (44886c)
by Rafael S.
07:26
created

WaveFile.toDataURI   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from 'bitdepth';
33
import * as imaadpcm from 'imaadpcm';
34
import * as alawmulaw from 'alawmulaw';
35
import {encode, decode} from 'base64-arraybuffer-es6';
36
import WaveFileParser from './lib/wavefile-parser';
37
import truncateSamples from './lib/truncate-samples';
38
import fixRIFFTag from './lib/fix-riff-tag';
39
import interleave from './lib/interleave';
40
import dwChannelMask from './lib/dw-channel-mask';
41
import {unpackArray, packArrayTo, unpackArrayTo, unpack,
42
  packTo} from 'byte-data';
43
44
/**
45
 * A class to read, write and process wav files.
46
 */
47
export default class WaveFile extends WaveFileParser {
48
49
  /**
50
   * @param {?Uint8Array=} wavBuffer A wave file buffer.
51
   * @throws {Error} If container is not RIFF, RIFX or RF64.
52
   * @throws {Error} If format is not WAVE.
53
   * @throws {Error} If no 'fmt ' chunk is found.
54
   * @throws {Error} If no 'data' chunk is found.
55
   */
56
  constructor(wavBuffer=null) {
57
    super();
58
    // Load a file from the buffer if one was passed
59
    // when creating the object
60
    if (wavBuffer) {
61
      this.fromBuffer(wavBuffer);
62
    }
63
  }
64
65
  /**
66
   * Set up the WaveFile object based on the arguments passed.
67
   * @param {number} numChannels The number of channels
68
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
69
   * @param {number} sampleRate The sample rate.
70
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
71
   * @param {string} bitDepthCode The audio bit depth code.
72
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
73
   *    or any value between '8' and '32' (like '12').
74
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
75
   *    The samples. Must be in the correct range according to the bit depth.
76
   * @param {?Object} options Optional. Used to force the container
77
   *    as RIFX with {'container': 'RIFX'}
78
   * @throws {Error} If any argument does not meet the criteria.
79
   */
80
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
81
    if (!options.container) {
82
      options.container = 'RIFF';
83
    }
84
    this.container = options.container;
85
    this.bitDepth = bitDepthCode;
86
    samples = interleave(samples);
87
    this.updateDataType_();
88
    /** @type {number} */
89
    let numBytes = this.dataType.bits / 8;
90
    this.data.samples = new Uint8Array(samples.length * numBytes);
91
    packArrayTo(samples, this.dataType, this.data.samples);
92
    this.clearHeader_();
93
    this.makeWavHeader_(
94
      bitDepthCode, numChannels, sampleRate,
95
      numBytes, this.data.samples.length, options);
96
    this.data.chunkId = 'data';
97
    this.data.chunkSize = this.data.samples.length;
98
    this.validateWavHeader_();
99
  }
100
101
  /**
102
   * Set up the WaveFile object from a byte buffer.
103
   * @param {!Uint8Array} wavBuffer The buffer.
104
   * @param {boolean=} samples True if the samples should be loaded.
105
   * @throws {Error} If container is not RIFF, RIFX or RF64.
106
   * @throws {Error} If format is not WAVE.
107
   * @throws {Error} If no 'fmt ' chunk is found.
108
   * @throws {Error} If no 'data' chunk is found.
109
   */
110
  fromBuffer(wavBuffer, samples=true) {
111
    this.readBuffer(wavBuffer, samples);
112
  }
113
114
  /**
115
   * Return a byte buffer representig the WaveFile object as a .wav file.
116
   * The return value of this method can be written straight to disk.
117
   * @return {!Uint8Array} A wav file.
118
   * @throws {Error} If bit depth is invalid.
119
   * @throws {Error} If the number of channels is invalid.
120
   * @throws {Error} If the sample rate is invalid.
121
   */
122
  toBuffer() {
123
    return this.writeBuffer();
124
  }
125
126
  /**
127
   * Force a file as RIFF.
128
   */
129
  toRIFF() {
130
    this.fromScratch(
131
      this.fmt.numChannels,
132
      this.fmt.sampleRate,
133
      this.bitDepth,
134
      unpackArray(this.data.samples, this.dataType));
135
  }
136
137
  /**
138
   * Force a file as RIFX.
139
   */
140
  toRIFX() {
141
    this.fromScratch(
142
      this.fmt.numChannels,
143
      this.fmt.sampleRate,
144
      this.bitDepth,
145
      unpackArray(this.data.samples, this.dataType),
146
      {container: 'RIFX'});
147
  }
148
149
  /**
150
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
151
   * @throws {Error} If sample rate is not 8000.
152
   * @throws {Error} If number of channels is not 1.
153
   */
154
  toIMAADPCM() {
155
    if (this.fmt.sampleRate !== 8000) {
156
      throw new Error(
157
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
158
    } else if (this.fmt.numChannels !== 1) {
159
      throw new Error(
160
        'Only mono files can be compressed as IMA-ADPCM.');
161
    } else {
162
      this.assure16Bit_();
163
      /** @type {!Int16Array} */
164
      let output = new Int16Array(this.data.samples.length / 2);
165
      unpackArrayTo(this.data.samples, this.dataType, output);
166
      this.fromScratch(
167
        this.fmt.numChannels,
168
        this.fmt.sampleRate,
169
        '4',
170
        imaadpcm.encode(output),
171
        {container: this.correctContainer_()});
172
    }
173
  }
174
175
  /**
176
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
177
   * @param {string} bitDepthCode The new bit depth of the samples.
178
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
179
   *    Optional. Default is 16.
180
   */
181
  fromIMAADPCM(bitDepthCode='16') {
182
    this.fromScratch(
183
      this.fmt.numChannels,
184
      this.fmt.sampleRate,
185
      '16',
186
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
187
      {container: this.correctContainer_()});
188
    if (bitDepthCode != '16') {
189
      this.toBitDepth(bitDepthCode);
190
    }
191
  }
192
193
  /**
194
   * Encode a 16-bit wave file as 8-bit A-Law.
195
   */
196
  toALaw() {
197
    this.assure16Bit_();
198
    /** @type {!Int16Array} */
199
    let output = new Int16Array(this.data.samples.length / 2);
200
    unpackArrayTo(this.data.samples, this.dataType, output);
201
    this.fromScratch(
202
      this.fmt.numChannels,
203
      this.fmt.sampleRate,
204
      '8a',
205
      alawmulaw.alaw.encode(output),
206
      {container: this.correctContainer_()});
207
  }
208
209
  /**
210
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
211
   * @param {string} bitDepthCode The new bit depth of the samples.
212
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
213
   *    Optional. Default is 16.
214
   */
215
  fromALaw(bitDepthCode='16') {
216
    this.fromScratch(
217
      this.fmt.numChannels,
218
      this.fmt.sampleRate,
219
      '16',
220
      alawmulaw.alaw.decode(this.data.samples),
221
      {container: this.correctContainer_()});
222
    if (bitDepthCode != '16') {
223
      this.toBitDepth(bitDepthCode);
224
    }
225
  }
226
227
  /**
228
   * Encode 16-bit wave file as 8-bit mu-Law.
229
   */
230
  toMuLaw() {
231
    this.assure16Bit_();
232
    /** @type {!Int16Array} */
233
    let output = new Int16Array(this.data.samples.length / 2);
234
    unpackArrayTo(this.data.samples, this.dataType, output);
235
    this.fromScratch(
236
      this.fmt.numChannels,
237
      this.fmt.sampleRate,
238
      '8m',
239
      alawmulaw.mulaw.encode(output),
240
      {container: this.correctContainer_()});
241
  }
242
243
  /**
244
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
245
   * @param {string} bitDepthCode The new bit depth of the samples.
246
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
247
   *    Optional. Default is 16.
248
   */
249
  fromMuLaw(bitDepthCode='16') {
250
    this.fromScratch(
251
      this.fmt.numChannels,
252
      this.fmt.sampleRate,
253
      '16',
254
      alawmulaw.mulaw.decode(this.data.samples),
255
      {container: this.correctContainer_()});
256
    if (bitDepthCode != '16') {
257
      this.toBitDepth(bitDepthCode);
258
    }
259
  }
260
261
  /**
262
   * Return the sample at a given index.
263
   * @param {number} index The sample index.
264
   * @return {number} The sample.
265
   * @throws {Error} If the sample index is off range.
266
   */
267
  getSample(index) {
268
    index = index * (this.dataType.bits / 8);
269
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
270
      throw new Error('Range error');
271
    }
272
    return unpack(
273
      this.data.samples.slice(index, index + this.dataType.bits / 8),
274
      this.dataType);
275
  }
276
277
  /**
278
   * Set the sample at a given index.
279
   * @param {number} index The sample index.
280
   * @param {number} sample The sample.
281
   * @throws {Error} If the sample index is off range.
282
   */
283
  setSample(index, sample) {
284
    index = index * (this.dataType.bits / 8);
285
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
286
      throw new Error('Range error');
287
    }
288
    packTo(sample, this.dataType, this.data.samples, index);
289
  }
290
291
  /**
292
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
293
   * @param {string} base64String A .wav file as a base64 string.
294
   * @throws {Error} If any property of the object appears invalid.
295
   */
296
  fromBase64(base64String) {
297
    this.fromBuffer(new Uint8Array(decode(base64String)));
298
  }
299
300
  /**
301
   * Return a base64 string representig the WaveFile object as a .wav file.
302
   * @return {string} A .wav file as a base64 string.
303
   * @throws {Error} If any property of the object appears invalid.
304
   */
305
  toBase64() {
306
    /** @type {!Uint8Array} */
307
    let buffer = this.toBuffer();
308
    return encode(buffer, 0, buffer.length);
309
  }
310
311
  /**
312
   * Return a DataURI string representig the WaveFile object as a .wav file.
313
   * The return of this method can be used to load the audio in browsers.
314
   * @return {string} A .wav file as a DataURI.
315
   * @throws {Error} If any property of the object appears invalid.
316
   */
317
  toDataURI() {
318
    return 'data:audio/wav;base64,' + this.toBase64();
319
  }
320
321
  /**
322
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
323
   * @param {string} dataURI A .wav file as DataURI.
324
   * @throws {Error} If any property of the object appears invalid.
325
   */
326
  fromDataURI(dataURI) {
327
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
328
  }
329
330
  /**
331
   * Change the bit depth of the samples.
332
   * @param {string} newBitDepth The new bit depth of the samples.
333
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
334
   * @param {boolean} changeResolution A boolean indicating if the
335
   *    resolution of samples should be actually changed or not.
336
   * @throws {Error} If the bit depth is not valid.
337
   */
338
  toBitDepth(newBitDepth, changeResolution=true) {
339
    /** @type {string} */
340
    let toBitDepth = newBitDepth;
341
    /** @type {string} */
342
    let thisBitDepth = this.bitDepth;
343
    if (!changeResolution) {
344
      if (newBitDepth != '32f') {
345
        toBitDepth = this.dataType.bits.toString();
346
      }
347
      thisBitDepth = this.dataType.bits;
348
    }
349
    this.assureUncompressed_();
350
    /** @type {number} */
351
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
352
    /** @type {!Float64Array} */
353
    let typedSamplesInput = new Float64Array(sampleCount);
354
    /** @type {!Float64Array} */
355
    let typedSamplesOutput = new Float64Array(sampleCount);
356
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
357
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
358
      truncateSamples(typedSamplesInput);
359
    }
360
    bitDepthLib(
361
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
362
    this.fromScratch(
363
      this.fmt.numChannels,
364
      this.fmt.sampleRate,
365
      newBitDepth,
366
      typedSamplesOutput,
367
      {container: this.correctContainer_()});
368
  }
369
370
  /**
371
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
372
   * then it is created. It if exists, it is overwritten.
373
   * @param {string} tag The tag name.
374
   * @param {string} value The tag value.
375
   * @throws {Error} If the tag name is not valid.
376
   */
377
  setTag(tag, value) {
378
    tag = fixRIFFTag(tag);
379
    /** @type {!Object} */
380
    let index = this.getTagIndex_(tag);
381
    if (index.TAG !== null) {
382
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
383
        value.length + 1;
384
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
385
    } else if (index.LIST !== null) {
386
      this.LIST[index.LIST].subChunks.push({
387
        chunkId: tag,
388
        chunkSize: value.length + 1,
389
        value: value});
390
    } else {
391
      this.LIST.push({
392
        chunkId: 'LIST',
393
        chunkSize: 8 + value.length + 1,
394
        format: 'INFO',
395
        subChunks: []});
396
      this.LIST[this.LIST.length - 1].subChunks.push({
397
        chunkId: tag,
398
        chunkSize: value.length + 1,
399
        value: value});
400
    }
401
  }
402
403
  /**
404
   * Return the value of a RIFF tag in the INFO chunk.
405
   * @param {string} tag The tag name.
406
   * @return {?string} The value if the tag is found, null otherwise.
407
   */
408
  getTag(tag) {
409
    /** @type {!Object} */
410
    let index = this.getTagIndex_(tag);
411
    if (index.TAG !== null) {
412
      return this.LIST[index.LIST].subChunks[index.TAG].value;
413
    }
414
    return null;
415
  }
416
417
  /**
418
   * Return a Object<tag, value> with the RIFF tags in the file.
419
   * @return {!Object<string, string>} The file tags.
420
   */
421
  listTags() {
422
    /** @type {?number} */
423
    let index = this.getLISTINFOIndex_();
424
    /** @type {!Object} */
425
    let tags = {};
426
    if (index !== null) {
427
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
428
        tags[this.LIST[index].subChunks[i].chunkId] =
429
          this.LIST[index].subChunks[i].value;
430
      }
431
    }
432
    return tags;
433
  }
434
435
  /**
436
   * Remove a RIFF tag in the INFO chunk.
437
   * @param {string} tag The tag name.
438
   * @return {boolean} True if a tag was deleted.
439
   */
440
  deleteTag(tag) {
441
    /** @type {!Object} */
442
    let index = this.getTagIndex_(tag);
443
    if (index.TAG !== null) {
444
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
445
      return true;
446
    }
447
    return false;
448
  }
449
450
  /**
451
   * Create a cue point in the wave file.
452
   * @param {number} position The cue point position in milliseconds.
453
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
454
   */
455
  setCuePoint(position, labl='') {
456
    this.cue.chunkId = 'cue ';
457
    position = (position * this.fmt.sampleRate) / 1000;
458
    /** @type {!Array<!Object>} */
459
    let existingPoints = this.getCuePoints_();
460
    this.clearLISTadtl_();
461
    /** @type {number} */
462
    let len = this.cue.points.length;
463
    this.cue.points = [];
464
    /** @type {boolean} */
465
    let hasSet = false;
466
    if (len === 0) {
467
      this.setCuePoint_(position, 1, labl);
468
    } else {
469
      for (let i = 0; i < len; i++) {
470
        if (existingPoints[i].dwPosition > position && !hasSet) {
471
          this.setCuePoint_(position, i + 1, labl);
472
          this.setCuePoint_(
473
            existingPoints[i].dwPosition,
474
            i + 2,
475
            existingPoints[i].label);
476
          hasSet = true;
477
        } else {
478
          this.setCuePoint_(
479
            existingPoints[i].dwPosition,
480
            i + 1,
481
            existingPoints[i].label);
482
        }
483
      }
484
      if (!hasSet) {
485
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
486
      }
487
    }
488
    this.cue.dwCuePoints = this.cue.points.length;
489
  }
490
491
  /**
492
   * Remove a cue point from a wave file.
493
   * @param {number} index the index of the point. First is 1,
494
   *    second is 2, and so on.
495
   */
496
  deleteCuePoint(index) {
497
    this.cue.chunkId = 'cue ';
498
    /** @type {!Array<!Object>} */
499
    let existingPoints = this.getCuePoints_();
500
    this.clearLISTadtl_();
501
    /** @type {number} */
502
    let len = this.cue.points.length;
503
    this.cue.points = [];
504
    for (let i = 0; i < len; i++) {
505
      if (i + 1 !== index) {
506
        this.setCuePoint_(
507
          existingPoints[i].dwPosition,
508
          i + 1,
509
          existingPoints[i].label);
510
      }
511
    }
512
    this.cue.dwCuePoints = this.cue.points.length;
513
    if (this.cue.dwCuePoints) {
514
      this.cue.chunkId = 'cue ';
515
    } else {
516
      this.cue.chunkId = '';
517
      this.clearLISTadtl_();
518
    }
519
  }
520
521
  /**
522
   * Return an array with all cue points in the file, in the order they appear
523
   * in the file.
524
   * The difference between this method and using the list in WaveFile.cue
525
   * is that the return value of this method includes the position in
526
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
527
   * @return {!Array<!Object>}
528
   */
529
  listCuePoints() {
530
    /** @type {!Array<!Object>} */
531
    let points = this.getCuePoints_();
532
    for (let i = 0, len = points.length; i < len; i++) {
533
      points[i].milliseconds =
534
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
535
    }
536
    return points;
537
  }
538
539
  /**
540
   * Update the label of a cue point.
541
   * @param {number} pointIndex The ID of the cue point.
542
   * @param {string} label The new text for the label.
543
   */
544
  updateLabel(pointIndex, label) {
545
    /** @type {?number} */
546
    let cIndex = this.getAdtlChunk_();
547
    if (cIndex !== null) {
548
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
549
        if (this.LIST[cIndex].subChunks[i].dwName ==
550
            pointIndex) {
551
          this.LIST[cIndex].subChunks[i].value = label;
552
        }
553
      }
554
    }
555
  }
556
557
  /**
558
   * Make the file 16-bit if it is not.
559
   * @private
560
   */
561
  assure16Bit_() {
562
    this.assureUncompressed_();
563
    if (this.bitDepth != '16') {
564
      this.toBitDepth('16');
565
    }
566
  }
567
568
  /**
569
   * Uncompress the samples in case of a compressed file.
570
   * @private
571
   */
572
  assureUncompressed_() {
573
    if (this.bitDepth == '8a') {
574
      this.fromALaw();
575
    } else if (this.bitDepth == '8m') {
576
      this.fromMuLaw();
577
    } else if (this.bitDepth == '4') {
578
      this.fromIMAADPCM();
579
    }
580
  }
581
  
582
  /**
583
   * Push a new cue point in this.cue.points.
584
   * @param {number} position The position in milliseconds.
585
   * @param {number} dwName the dwName of the cue point
586
   * @private
587
   */
588
  setCuePoint_(position, dwName, label) {
589
    this.cue.points.push({
590
      dwName: dwName,
591
      dwPosition: position,
592
      fccChunk: 'data',
593
      dwChunkStart: 0,
594
      dwBlockStart: 0,
595
      dwSampleOffset: position,
596
    });
597
    this.setLabl_(dwName, label);
598
  }
599
600
  /**
601
   * Return an array with all cue points in the file, in the order they appear
602
   * in the file.
603
   * @return {!Array<!Object>}
604
   * @private
605
   */
606
  getCuePoints_() {
607
    /** @type {!Array<!Object>} */
608
    let points = [];
609
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
610
      points.push({
611
        dwPosition: this.cue.points[i].dwPosition,
612
        label: this.getLabelForCuePoint_(
613
          this.cue.points[i].dwName)});
614
    }
615
    return points;
616
  }
617
618
  /**
619
   * Return the label of a cue point.
620
   * @param {number} pointDwName The ID of the cue point.
621
   * @return {string}
622
   * @private
623
   */
624
  getLabelForCuePoint_(pointDwName) {
625
    /** @type {?number} */
626
    let cIndex = this.getAdtlChunk_();
627
    if (cIndex !== null) {
628
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
629
        if (this.LIST[cIndex].subChunks[i].dwName ==
630
            pointDwName) {
631
          return this.LIST[cIndex].subChunks[i].value;
632
        }
633
      }
634
    }
635
    return '';
636
  }
637
638
  /**
639
   * Clear any LIST chunk labeled as 'adtl'.
640
   * @private
641
   */
642
  clearLISTadtl_() {
643
    for (let i = 0, len = this.LIST.length; i < len; i++) {
644
      if (this.LIST[i].format == 'adtl') {
645
        this.LIST.splice(i);
646
      }
647
    }
648
  }
649
650
  /**
651
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
652
   * @param {number} dwName The ID of the cue point.
653
   * @param {string} label The label for the cue point.
654
   * @private
655
   */
656
  setLabl_(dwName, label) {
657
    /** @type {?number} */
658
    let adtlIndex = this.getAdtlChunk_();
659
    if (adtlIndex === null) {
660
      this.LIST.push({
661
        chunkId: 'LIST',
662
        chunkSize: 4,
663
        format: 'adtl',
664
        subChunks: []});
665
      adtlIndex = this.LIST.length - 1;
666
    }
667
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
668
  }
669
670
  /**
671
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
672
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
673
   * @param {number} dwName The ID of the cue point.
674
   * @param {string} label The label for the cue point.
675
   * @private
676
   */
677
  setLabelText_(adtlIndex, dwName, label) {
678
    this.LIST[adtlIndex].subChunks.push({
679
      chunkId: 'labl',
680
      chunkSize: label.length,
681
      dwName: dwName,
682
      value: label
683
    });
684
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
685
  }
686
687
  /**
688
   * Return the index of the 'adtl' LIST in this.LIST.
689
   * @return {?number}
690
   * @private
691
   */
692
  getAdtlChunk_() {
693
    for (let i = 0, len = this.LIST.length; i < len; i++) {
694
      if (this.LIST[i].format == 'adtl') {
695
        return i;
696
      }
697
    }
698
    return null;
699
  }
700
701
  /**
702
   * Return the index of the INFO chunk in the LIST chunk.
703
   * @return {?number} the index of the INFO chunk.
704
   * @private
705
   */
706
  getLISTINFOIndex_() {
707
    /** @type {?number} */
708
    let index = null;
709
    for (let i = 0, len = this.LIST.length; i < len; i++) {
710
      if (this.LIST[i].format === 'INFO') {
711
        index = i;
712
        break;
713
      }
714
    }
715
    return index;
716
  }
717
718
  /**
719
   * Return the index of a tag in a FILE chunk.
720
   * @param {string} tag The tag name.
721
   * @return {!Object<string, ?number>}
722
   *    Object.LIST is the INFO index in LIST
723
   *    Object.TAG is the tag index in the INFO
724
   * @private
725
   */
726
  getTagIndex_(tag) {
727
    /** @type {!Object<string, ?number>} */
728
    let index = {LIST: null, TAG: null};
729
    for (let i = 0, len = this.LIST.length; i < len; i++) {
730
      if (this.LIST[i].format == 'INFO') {
731
        index.LIST = i;
732
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
733
          if (this.LIST[i].subChunks[j].chunkId == tag) {
734
            index.TAG = j;
735
            break;
736
          }
737
        }
738
        break;
739
      }
740
    }
741
    return index;
742
  }
743
744
  /**
745
   * Return 'RIFF' if the container is 'RF64', the current container name
746
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
747
   * @return {string}
748
   * @private
749
   */
750
  correctContainer_() {
751
    return this.container == 'RF64' ? 'RIFF' : this.container;
752
  }
753
754
  /**
755
   * Define the header of a wav file.
756
   * @param {string} bitDepthCode The audio bit depth
757
   * @param {number} numChannels The number of channels
758
   * @param {number} sampleRate The sample rate.
759
   * @param {number} numBytes The number of bytes each sample use.
760
   * @param {number} samplesLength The length of the samples in bytes.
761
   * @param {!Object} options The extra options, like container defintion.
762
   * @private
763
   */
764
  makeWavHeader_(
765
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
766
    if (bitDepthCode == '4') {
767
      this.createADPCMHeader_(
768
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
769
770
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
771
      this.createALawMulawHeader_(
772
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
773
774
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
775
        numChannels > 2) {
776
      this.createExtensibleHeader_(
777
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
778
779
    } else {
780
      this.createPCMHeader_(
781
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
782
    }
783
  }
784
785
  /**
786
   * Create the header of a linear PCM wave file.
787
   * @param {string} bitDepthCode The audio bit depth
788
   * @param {number} numChannels The number of channels
789
   * @param {number} sampleRate The sample rate.
790
   * @param {number} numBytes The number of bytes each sample use.
791
   * @param {number} samplesLength The length of the samples in bytes.
792
   * @param {!Object} options The extra options, like container defintion.
793
   * @private
794
   */
795
  createPCMHeader_(
796
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
797
    this.container = options.container;
798
    this.chunkSize = 36 + samplesLength;
799
    this.format = 'WAVE';
800
    this.bitDepth = bitDepthCode;
801
    this.fmt = {
802
      chunkId: 'fmt ',
803
      chunkSize: 16,
804
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
805
      numChannels: numChannels,
806
      sampleRate: sampleRate,
807
      byteRate: (numChannels * numBytes) * sampleRate,
808
      blockAlign: numChannels * numBytes,
809
      bitsPerSample: parseInt(bitDepthCode, 10),
810
      cbSize: 0,
811
      validBitsPerSample: 0,
812
      dwChannelMask: 0,
813
      subformat: []
814
    };
815
  }
816
817
  /**
818
   * Create the header of a ADPCM wave file.
819
   * @param {string} bitDepthCode The audio bit depth
820
   * @param {number} numChannels The number of channels
821
   * @param {number} sampleRate The sample rate.
822
   * @param {number} numBytes The number of bytes each sample use.
823
   * @param {number} samplesLength The length of the samples in bytes.
824
   * @param {!Object} options The extra options, like container defintion.
825
   * @private
826
   */
827
  createADPCMHeader_(
828
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
829
    this.createPCMHeader_(
830
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
831
    this.chunkSize = 40 + samplesLength;
832
    this.fmt.chunkSize = 20;
833
    this.fmt.byteRate = 4055;
834
    this.fmt.blockAlign = 256;
835
    this.fmt.bitsPerSample = 4;
836
    this.fmt.cbSize = 2;
837
    this.fmt.validBitsPerSample = 505;
838
    this.fact = {
839
      chunkId: 'fact',
840
      chunkSize: 4,
841
      dwSampleLength: samplesLength * 2
842
    };
843
  }
844
845
  /**
846
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
847
   * @param {string} bitDepthCode The audio bit depth
848
   * @param {number} numChannels The number of channels
849
   * @param {number} sampleRate The sample rate.
850
   * @param {number} numBytes The number of bytes each sample use.
851
   * @param {number} samplesLength The length of the samples in bytes.
852
   * @param {!Object} options The extra options, like container defintion.
853
   * @private
854
   */
855
  createExtensibleHeader_(
856
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
857
    this.createPCMHeader_(
858
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
859
    this.chunkSize = 36 + 24 + samplesLength;
860
    this.fmt.chunkSize = 40;
861
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
862
    this.fmt.cbSize = 22;
863
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
864
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
865
    // subformat 128-bit GUID as 4 32-bit values
866
    // only supports uncompressed integer PCM samples
867
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
868
  }
869
870
  /**
871
   * Create the header of mu-Law and A-Law wave files.
872
   * @param {string} bitDepthCode The audio bit depth
873
   * @param {number} numChannels The number of channels
874
   * @param {number} sampleRate The sample rate.
875
   * @param {number} numBytes The number of bytes each sample use.
876
   * @param {number} samplesLength The length of the samples in bytes.
877
   * @param {!Object} options The extra options, like container defintion.
878
   * @private
879
   */
880
  createALawMulawHeader_(
881
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
882
    this.createPCMHeader_(
883
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
884
    this.chunkSize = 40 + samplesLength;
885
    this.fmt.chunkSize = 20;
886
    this.fmt.cbSize = 2;
887
    this.fmt.validBitsPerSample = 8;
888
    this.fact = {
889
      chunkId: 'fact',
890
      chunkSize: 4,
891
      dwSampleLength: samplesLength
892
    };
893
  }
894
}
895